﻿using System;
using System.Collections.Generic;
using System.Linq;
using Simp.Model.Exceptions;

namespace Simp.Model.CoreTransformations
{
	/// <summary>Bind known expressions.</summary>
	public class ExpressionBindingTransformation : IExpressionTransformation
	{
		private readonly bool _isToleratingFailure;
		private readonly IDictionary<string, IExpression> _knownExpressionMap;
		private readonly string[] _knownParameterNameList;

		/// <summary>Construct a transformation with a list of known expressions and a list of known parameter names.</summary>
		/// <param name="isToleratingFailure"/>
		/// <param name="knownExpressions"/>
		/// <param name="knownParameterNameList"/>
		public ExpressionBindingTransformation(
			bool isToleratingFailure,
			IExpression[] knownExpressions,
			params string[] knownParameterNameList)
		{
			if (knownExpressions == null)
			{
				throw new ArgumentNullException("knownExpressions");
			}
			if (knownParameterNameList == null)
			{
				throw new ArgumentNullException("knownParameterNameList");
			}
			_isToleratingFailure = isToleratingFailure;
			_knownExpressionMap = new Dictionary<string, IExpression>();

			foreach (IExpression expression in knownExpressions)
			{
				CustomExpression custom;
				CompositeExpression composite = ExpressionHelper.TryGetComposite(expression, out custom);

				if (composite != null)
				{
					INamedExpression namedExpression = custom;

					_knownExpressionMap.Add(namedExpression.Name, expression);
				}
				else
				{
					INamedExpression namedExpression = ExpressionHelper.GetProxy(expression);

					_knownExpressionMap.Add(namedExpression.Name, expression);
				}
			}

			_knownParameterNameList = (string[])knownParameterNameList.Clone();
		}

		/// <summary>Construct a transformation with a map of known expressions and a list of known parameter names.</summary>
		/// <param name="isToleratingFailure"/>
		/// <param name="knownExpressionMap"/>
		/// <param name="knownParameterNameList"/>
		public ExpressionBindingTransformation(
			bool isToleratingFailure,
			IDictionary<string, IExpression> knownExpressionMap,
			params string[] knownParameterNameList)
		{
			if (knownExpressionMap == null)
			{
				throw new ArgumentNullException("knownExpressionIndex");
			}
			if (knownParameterNameList == null)
			{
				throw new ArgumentNullException("knownParameterNameList");
			}
			_isToleratingFailure = isToleratingFailure;
			_knownExpressionMap = new Dictionary<string, IExpression>(knownExpressionMap);
			_knownParameterNameList = (string[])knownParameterNameList.Clone();
		}

		#region IExpressionTransformation Members
		IExpression IExpressionTransformation.Tranform(IExpression expression)
		{
			if (_knownExpressionMap.Count == 0)
			{	//	No substitution to do
				return null;
			}
			else
			{
				CompositeExpression composite = expression as CompositeExpression;

				if (composite == null)
				{
					UnboundCallingExpression unbound = expression as UnboundCallingExpression;

					if (unbound == null)
					{	//	End of recursion:  we hit a leaf
						return null;
					}
					else
					{	//	Unbound expression
						if (((IParameterPolicyExpression)unbound).ParameterBindings.Length > 0)
						{
							throw new ApplicationException("We shouldn't get to this point with parameters, we should have caught it as a composite");
						}

						return BindUnboundExpression(unbound);
					}
				}
				else
				{
					UnboundCallingExpression unbound = composite.InnerExpression as UnboundCallingExpression;

					if (unbound != null)
					{	//	Unbound expression
						return BindUnboundComposition(composite, unbound);
					}
					else
					{
						CustomExpression custom = composite.InnerExpression as CustomExpression;

						if (custom != null)
						{
							return BindCustomExpression(composite, custom);
						}
						else
						{
							return ExpressionHelper.TranformComposite(true, this, composite);
						}
					}
				}
			}
		}
		#endregion

		private IExpression BindUnboundExpression(UnboundCallingExpression unbound)
		{
			string unboundName = ((INamedExpression)unbound).Name;
			IExpression knownExpression;

			if (_knownExpressionMap.TryGetValue(unboundName, out knownExpression))
			{
				return knownExpression;
			}
			else if (((IList<string>)_knownParameterNameList).Contains(unboundName))
			{	//	We hit a parameter, which will remain unbound
				return null;
			}
			else if (_isToleratingFailure)
			{
				return null;
			}
			else
			{
				throw new UnknownFunctionException(unboundName);
			}
		}

		private IExpression BindUnboundComposition(CompositeExpression compositeExpression, UnboundCallingExpression unbound)
		{
			IExpression[] transformedParameterList =
				ExpressionHelper.Tranform(true, this, compositeExpression.ParameterList);
			IExpression boundExpression = BindUnboundExpression(unbound);

			if (boundExpression != null)
			{
				CompositeExpression knownComposite = ExpressionHelper.TryGetComposite<CustomExpression>(boundExpression);

				if (knownComposite == null)
				{	//	Not a composite expression, we return it
					return new CompositeExpression(
						boundExpression,
						ExpressionHelper.DefaultIfNull(transformedParameterList, compositeExpression.ParameterList));
				}
				else if (knownComposite.ParameterList.Length > 1)
				{	//	Expression requiring parameters
					IExpression[] expressionParameters =
						ExpressionHelper.DefaultIfNull(transformedParameterList, compositeExpression.ParameterList);
					IExpression[] callingParameters = new IExpression[expressionParameters.Length + 1];

					callingParameters[0] = boundExpression;
					Array.Copy(expressionParameters, 0, callingParameters, 1, callingParameters.Length - 1);

					return new CompositeExpression(
						new BoundCallingExpression(knownComposite.ParameterList.Length),
						callingParameters);
				}
				else
				{	//	Parameter-less expression:  we return the body
					return knownComposite.ParameterList[0];
				}
			}
			else
			{
				if (transformedParameterList == null)
				{
					return null;
				}
				else
				{
					return new CompositeExpression(unbound, transformedParameterList);
				}
			}
		}

		private IExpression BindCustomExpression(CompositeExpression composite, CustomExpression custom)
		{
			if (composite.ParameterList.Length < 1)
			{
				return null;
			}
			else
			{
				IExpression newBody = BindCustomBody(composite, custom, custom.IsLambda);
				IExpression[] newParameterBindings = BindCustomParameterBinding(composite);

				if (newBody == null && newParameterBindings == null)
				{
					return null;
				}
				else
				{
					IExpression[] newParameters = new IExpression[composite.ParameterList.Length];

					newParameters[0] = ExpressionHelper.DefaultIfNull(newBody, composite.ParameterList[0]);

					if (newParameterBindings == null)
					{
						Array.Copy(composite.ParameterList, 1, newParameters, 1, newParameters.Length - 1);
					}
					else
					{
						Array.Copy(newParameterBindings, 0, newParameters, 1, newParameters.Length - 1);
					}

					CustomExpression newCustom = new CustomExpression(
						((INamedExpression)custom).Name,
						custom.IsLambda,
						((IParameterPolicyExpression)custom).ParameterBindings.Length);

					return new CompositeExpression(newCustom, newParameters);
				}
			}
		}

		private IExpression BindCustomBody(CompositeExpression composite, CustomExpression custom, bool isLambda)
		{
			IParameterBinding[] parameterBindings = custom.GetParameterBinding(composite.ParameterList);
			var parameterList = from binding in parameterBindings select binding.Name;
			//	If is lambda, we keep the ambiant known parameters
			string[] newKnownParameterNameList = isLambda
				? _knownParameterNameList.Union(parameterList).ToArray()
				: parameterList.ToArray();
			IExpressionTransformation transformation =
				new ExpressionBindingTransformation(_isToleratingFailure, _knownExpressionMap, newKnownParameterNameList);
			IExpression currentBody = composite.ParameterList[0];
			IExpression newBody = transformation.Tranform(currentBody);

			return newBody;
		}

		private IExpression[] BindCustomParameterBinding(CompositeExpression composite)
		{
			bool hasChanged = false;
			IExpression[] newParameterBinding = new IExpression[composite.ParameterList.Length - 1];

			for (int i = 0; i != newParameterBinding.Length; ++i)
			{
				IExpression currentParam = composite.ParameterList[i + 1];
				IExpression newParam = ((IExpressionTransformation)this).Tranform(currentParam);

				newParameterBinding[i] = ExpressionHelper.DefaultIfNull(newParam, currentParam);
				if (newParam != null)
				{
					hasChanged = true;
				}
			}

			return hasChanged ? newParameterBinding : null;
		}
	}
}